// SPDX-License-Identifier: GPL-2.0-or-later
/* Driver for Virtio QTBox device.
 *
 * Copyright 2022 HUAWEI TECHNOLOGIES CO., LTD.
 */

#include <linux/err.h>
#include <linux/module.h>
#include <linux/virtio_config.h>
#include <linux/slab.h>
#include "uapi/virtio_qtbox.h"
#include "virtio_qtbox_comm.h"
#ifdef _LINUX_VIRTIO_IDS_H
#undef _LINUX_VIRTIO_IDS_H
#include "uapi/virtio_ids.h"
#endif

static LIST_HEAD(virtio_qtbox_devs);
static uint32_t num_devs;
static DEFINE_MUTEX(devs_mutex);

static int virtio_qtbox_add_dev(struct virtio_qtbox *qtbox)
{
	struct virtio_qtbox *iter;

	mutex_lock(&devs_mutex);

	list_for_each_entry(iter, &virtio_qtbox_devs, node) {
		if (iter == qtbox) {
			dev_err(&qtbox->vdev->dev, "%s failure: qtbox has been added\n", __func__);
			mutex_unlock(&devs_mutex);
			return -EEXIST;
		}
	}

	list_add_tail(&qtbox->node, &virtio_qtbox_devs);
	num_devs++;
	mutex_unlock(&devs_mutex);
	dev_info(&qtbox->vdev->dev, "%s success: qtbox add device\n", __func__);
	return 0;
}

static void virtio_qtbox_remove_dev(struct virtio_qtbox *qtbox)
{
	struct virtio_qtbox *iter;

	mutex_lock(&devs_mutex);

	list_for_each_entry(iter, &virtio_qtbox_devs, node) {
		if (iter == qtbox) {
			dev_info(&qtbox->vdev->dev, "%s success: qtbox remove device\n", __func__);
			list_del(&qtbox->node);
			num_devs--;
			break;
		}
	}

	mutex_unlock(&devs_mutex);
}

static int virtio_qtbox_init_vqs(struct virtio_qtbox *qtbox)
{
	struct virtqueue **vqs;
	vq_callback_t **callbacks;
	const char **names;
	int total_vqs;
	int ret = -ENOMEM;
	int i;

	/*
	 * [0]  cmd queue
	 * [1]  event queue
	 * [2..N-1]  data queues
	 */
	total_vqs = qtbox->max_dataqs + 2;

	vqs = kcalloc(total_vqs, sizeof(*vqs), GFP_KERNEL);
	if (!vqs)
		goto err_vqs;

	callbacks = kcalloc(total_vqs, sizeof(*callbacks), GFP_KERNEL);
	if (!callbacks)
		goto err_callbacks;

	names = kcalloc(total_vqs, sizeof(*names), GFP_KERNEL);
	if (!names)
		goto err_names;

	if (qtbox->max_dataqs) {
		qtbox->dataq = kcalloc(qtbox->max_dataqs, sizeof(*qtbox->dataq),
				       GFP_KERNEL);
		if (!qtbox->dataq)
			goto err_dataq;
	}

	callbacks[0] = NULL;
	names[0] = "qtbox_cmdq";
	callbacks[1] = virtio_qtbox_event_cb;
	names[1] = "qtbox_eventq";

	for (i = 2; i < total_vqs; i++) {
		int k = i - 2;
		callbacks[i] = virtio_qtbox_data_cb;
		(void)snprintf(qtbox->dataq[k].name, sizeof(qtbox->dataq[k].name),
			           "qtbox_dataq.%d", k);
		names[i] = qtbox->dataq[k].name;
		mutex_init(&qtbox->dataq[k].dataq_mutex);
	}

	ret = virtio_find_vqs(qtbox->vdev, total_vqs, vqs, callbacks, names, NULL);
	if (ret) {
		dev_err(&qtbox->vdev->dev, "%s failure: vqs find err\n", __func__);
		goto err_find;
	}

	qtbox->cmdq = vqs[0];
	qtbox->eventq = vqs[1];
	for (i = 2; i < total_vqs; i++)
		qtbox->dataq[i - 2].vq = vqs[i];

	/* add elem to eventq */
	if (!virtio_qtbox_eventq_fill(qtbox->eventq)) {
		dev_err(&qtbox->vdev->dev, "%s failure: eventq fill err\n", __func__);
		virtio_qtbox_eventq_drain(qtbox->eventq);
		qtbox->vdev->config->del_vqs(qtbox->vdev);
		ret = -EFAULT;
	}

	/* add elem to dataq */
	for (i = 2; i < total_vqs; i++) {
		if (!virtio_qtbox_dataq_fill(&qtbox->dataq[i - 2])) {
			dev_err(&qtbox->vdev->dev, "%s failure: dataq fill err\n", __func__);
			virtio_qtbox_dataq_drain(qtbox);
			qtbox->vdev->config->del_vqs(qtbox->vdev);
			ret = -EFAULT;
			break;
		}
	}

	if (!ret)
		dev_info(&qtbox->vdev->dev, "%s success: qtbox init vqs\n", __func__);

err_find:
	if (ret && qtbox->dataq)
		kfree(qtbox->dataq);
err_dataq:
	kfree(names);
err_names:
	kfree(callbacks);
err_callbacks:
	kfree(vqs);
err_vqs:
	return ret;
}

static void virtio_qtbox_deinit_vqs(struct virtio_qtbox *qtbox)
{
	virtio_qtbox_eventq_drain(qtbox->eventq);
	virtio_qtbox_dataq_drain(qtbox);

	qtbox->vdev->config->del_vqs(qtbox->vdev);
	kfree(qtbox->dataq);
	dev_info(&qtbox->vdev->dev, "%s success: qtbox deinit vqs\n", __func__);
}

static int virtio_qtbox_probe(struct virtio_device *vdev)
{
	struct virtio_qtbox *qtbox;
	int err;

	err = os_compat_init();
	if (err)
		return err;

	if (!virtio_has_feature(vdev, VIRTIO_F_VERSION_1))
		return -ENODEV;

	if (!vdev->config->get) {
		dev_err(&vdev->dev, "%s failure: config access disabled\n", __func__);
		return -EINVAL;
	}

	qtbox = kzalloc(sizeof(*qtbox), GFP_KERNEL);
	if (!qtbox) {
		dev_err(&vdev->dev, "%s failure: alloc qtbox\n", __func__);
		return -ENOMEM;
	}

	virtio_cread(vdev, struct virtio_qtbox_config, max_dataqueues, &qtbox->max_dataqs);

	mutex_init(&qtbox->cmdq_mutex);

	mutex_init(&qtbox->eventq_mutex);
	qtbox->event_wq = create_singlethread_workqueue("qtbox_event_wq");
	if (!qtbox->event_wq) {
		dev_err(&vdev->dev, "%s failure: alloc event wq\n", __func__);
		err = -ENOMEM;
		goto err_event_wq;
	}

	qtbox->data_wq = create_singlethread_workqueue("qtbox_data_wq");
	if (!qtbox->data_wq) {
		dev_err(&vdev->dev, "%s failure: alloc data wq\n", __func__);
		err = -ENOMEM;
		goto err_data_wq;
	}

	qtbox->release_wq = create_singlethread_workqueue("qtbox_release_wq");
	if (!qtbox->release_wq) {
		dev_err(&vdev->dev, "%s failure: alloc release_wq\n", __func__);
		goto err_release_wq;
	}

	INIT_WORK(&qtbox->notify_work, virtio_qtbox_event_work_handler);
	INIT_WORK(&qtbox->notify_dataq_work, virtio_qtbox_data_work_handler);

	vdev->priv = qtbox;
	qtbox->vdev = vdev;

	err = virtio_qtbox_init_vqs(qtbox);
	if (err) {
		dev_err(&vdev->dev, "%s failure: init vqs err\n", __func__);
		goto err_vqs;
	}

	virtio_device_ready(vdev);

	if (virtio_has_feature(vdev, VIRTIO_QTBOX_F_SANDBOX_VM)) {
		err = virtio_qtbox_sandbox_vm_init(&qtbox->sandbox);
		if (err) {
			dev_err(&vdev->dev, "%s failure: qtbox vm init err\n", __func__);
			goto err_sandbox_init;
		}

		qtbox->support_sandbox_vm = true;
	}

	err = virtio_qtbox_add_dev(qtbox);
	if (err) {
		dev_err(&vdev->dev, "%s failure: add qtbox device err\n", __func__);
		goto err_add_dev;
	}

	dev_info(&vdev->dev, "%s success: qtbox device probe done\n", __func__);
	return 0;

err_add_dev:
	if (qtbox->support_sandbox_vm)
		virtio_qtbox_sandbox_vm_uninit(&qtbox->sandbox);
err_sandbox_init:
	vdev->config->reset(vdev);
	virtio_qtbox_deinit_vqs(qtbox);
err_vqs:
	destroy_workqueue(qtbox->release_wq);
err_release_wq:
	destroy_workqueue(qtbox->data_wq);
err_data_wq:
	destroy_workqueue(qtbox->event_wq);
err_event_wq:
	kfree(qtbox);
	return err;
}

static void virtio_qtbox_remove(struct virtio_device *vdev)
{
	struct virtio_qtbox *qtbox = vdev->priv;

	virtio_qtbox_remove_dev(qtbox);
	if (qtbox->support_sandbox_vm)
		virtio_qtbox_sandbox_vm_uninit(&qtbox->sandbox);
	vdev->config->reset(vdev);
	flush_work(&qtbox->notify_work);
	flush_workqueue(qtbox->event_wq);
	destroy_workqueue(qtbox->event_wq);
	flush_work(&qtbox->notify_dataq_work);
	flush_workqueue(qtbox->data_wq);
	destroy_workqueue(qtbox->data_wq);
	flush_workqueue(qtbox->release_wq);
	destroy_workqueue(qtbox->release_wq);
	virtio_qtbox_deinit_vqs(qtbox);
	dev_info(&vdev->dev, "%s success: qtbox device remove done\n", __func__);
	kfree(qtbox);
}

static void virtio_qtbox_config_changed(struct virtio_device *vdev)
{
	/* TODO */
}

static const unsigned int features[] = {
	VIRTIO_QTBOX_F_SANDBOX_VM,
};

static const struct virtio_device_id id_table[] = {
	{ VIRTIO_ID_QTBOX, VIRTIO_DEV_ANY_ID },
	{ 0 },
};

static struct virtio_driver virtio_qtbox_driver = {
	.driver.name         = KBUILD_MODNAME,
	.driver.owner        = THIS_MODULE,
	.feature_table       = features,
	.feature_table_size  = ARRAY_SIZE(features),
	.id_table            = id_table,
	.probe               = virtio_qtbox_probe,
	.remove              = virtio_qtbox_remove,
	.config_changed      = virtio_qtbox_config_changed,
};

module_virtio_driver(virtio_qtbox_driver);

MODULE_DEVICE_TABLE(virtio, id_table);
MODULE_DESCRIPTION("virtio qtbox device driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Longpeng <longpeng2@huawei.com>");
